package vip.liux.gateway.config;

import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.springdoc.core.properties.AbstractSwaggerUiConfigProperties;
import org.springdoc.core.properties.SwaggerUiConfigProperties;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.toList;

@Configuration(proxyBeanMethods = false)
public class DocConfiguration implements ApplicationEventPublisherAware {
    private static final String DISCOVERY_CLIENT_ID_PRE = "ReactiveCompositeDiscoveryClient_";
    private final Logger log = org.slf4j.LoggerFactory.getLogger(DocConfiguration.class);

    @Value("${spring.application.name}")
    private String selfServiceName;

    private final RouteDefinitionWriter routeDefinitionWriter;
    private final RouteDefinitionLocator locator;
    protected final SwaggerUiConfigProperties swaggerUiConfigProperties;

    private ApplicationEventPublisher applicationEventPublisher;

    public DocConfiguration(RouteDefinitionWriter routeDefinitionWriter, RouteDefinitionLocator locator, SwaggerUiConfigProperties swaggerUiConfigProperties) {
        this.routeDefinitionWriter = routeDefinitionWriter;
        this.locator = locator;
        this.swaggerUiConfigProperties = swaggerUiConfigProperties;
    }

    @Bean
    public OpenAPI springShopOpenAPI() {
        return new OpenAPI()
                .info(new Info().title("Spring Boot 中使用 Swagger UI 构建 restful API")
                        .contact(new Contact())
                        .description("信息管理平台提供的 restful API")
                        .version("v1.0.0")
                        .license(new License().name("Apache 2.0").url("https://spring.io")))
                .externalDocs(new ExternalDocumentation()
                        .description("外部文档")
                        .url("https://docs.spring.io"));
    }

    @PostConstruct
    public void init() {
        installAllDownstreamServiceApiDocsRoutes();
        autoInitSwaggerUrls();
    }

    public void autoInitSwaggerUrls() {
        List<RouteDefinition> definitions = ofNullable(locator.getRouteDefinitions().collectList().block())
                .orElse(Collections.emptyList());
        final String selfServiceId = DISCOVERY_CLIENT_ID_PRE + selfServiceName;
        definitions.stream()
                .filter(routeDefinition -> ofNullable(routeDefinition.getId())
                        //只保留服务级别的路由。
                        .filter(id -> id.startsWith(DISCOVERY_CLIENT_ID_PRE))
                        //排除本系统。
                        .filter(id -> !selfServiceId.equalsIgnoreCase(id))
                        .isPresent())
                .forEach(routeDefinition -> {
                    AbstractSwaggerUiConfigProperties.SwaggerUrl swaggerUrl = new AbstractSwaggerUiConfigProperties.SwaggerUrl(
                            routeDefinition.getId().replace(DISCOVERY_CLIENT_ID_PRE, "").toLowerCase(),
                            routeDefinition.getUri().toString().replace("lb://", "").toLowerCase() + "/v3/api-docs",
                            ""
                    );
                    Set<AbstractSwaggerUiConfigProperties.SwaggerUrl> urls = swaggerUiConfigProperties.getUrls();
                    if (urls == null) {
                        urls = new LinkedHashSet<>();
                        swaggerUiConfigProperties.setUrls(urls);
                    }
                    urls.add(swaggerUrl);
                });
    }

    /**
     * 安装所有下游服务的apidocs路由。
     */
    private void installAllDownstreamServiceApiDocsRoutes() {
        List<RouteDefinition> definitions = ofNullable(locator.getRouteDefinitions().collectList().block())
                .orElse(Collections.emptyList());
        final String selfServiceId = DISCOVERY_CLIENT_ID_PRE + selfServiceName;
        //解析出所有子服务名。
        List<String> services = definitions.stream()
                .filter(routeDefinition -> ofNullable(routeDefinition.getId())
                        //只保留服务级别的路由.
                        .filter(id -> id.startsWith(DISCOVERY_CLIENT_ID_PRE))
                        //排除本系统。
                        .filter(id -> !selfServiceId.equalsIgnoreCase(id))
                        .isPresent()
                )
                .map(routeDefinition -> routeDefinition.getUri().toString().replace("lb://", "").toLowerCase())
                .collect(toList());
        services.forEach(this::installRoute);
        if (!services.isEmpty() && log.isInfoEnabled()) {
            log.info("自动安装了{}个子服务的apidocs路由：{}", services.size(),
                    String.join(",", services));
        }
    }


    /**
     * 安装一个子服务的apidocs路由。
     *
     * @param serviceName 服务名称
     */
    private void installRoute(String serviceName) {
        RouteDefinition routeDefinition = new RouteDefinition();
        routeDefinition.setId(serviceName + "-apidocs");
        routeDefinition.setUri(URI.create("lb://" + serviceName));
        routeDefinition.setPredicates(Collections.singletonList(new PredicateDefinition("Path=/" + serviceName + "/v3/api-docs/**")));
        routeDefinition.setFilters(Collections.singletonList(new FilterDefinition("StripPrefix=1")));
        routeDefinition.setOrder(-1);
        routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe();
        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }
}
